package org.joone.engine;

import java.io.*;
import java.util.TreeSet;
import java.util.Collection;
import java.util.ArrayList;

import org.joone.io.InputSwitchSynapse;
import org.joone.log.*;
import org.joone.inspection.Inspectable;
import org.joone.inspection.implementations.WeightsInspection;

/**
 * The Synapse is the connection element between two Layer objects. Its
 * connections are represented by weights that transport the patterns from a
 * layer to another. These weights are modified in the learning cycles, and
 * represent the 'memory' of the trained neural net.
 */
public abstract class Synapse
		implements InputPatternListener, OutputPatternListener, LearnableSynapse, Serializable, Inspectable {

    private static final long serialVersionUID = -5892822057908231022L;

	private static final ILogger log = LoggerFactory.getLogger(Synapse.class);
	/** Count of synapses for naming purposes. */
	private static int synapseCount = 0;

	/** Name set by default to "Synapse [synapse count]" */
	private String fieldName = "Synapse " + ++synapseCount;

	private double learningRate = 0;
	private double momentum = 0;
	private int inputDimension = 0;
	private int outputDimension = 0;
	private boolean inputFull;
	private boolean outputFull;
	private Monitor monitor;
	private int ignoreBefore = -1; // not more used
	private boolean loopBack = false;
	// true if this synapse closes a loop of a recurrent neural network

	protected Matrix array;
	protected int m_batch = 0;
	protected boolean enabled = true;
	protected transient double[] inps = null;
	protected transient double[] outs = null;
	protected transient double[] bouts;
	protected transient int items = 0;
	protected transient int bitems = 0;

	/**
	 * @label revPattern
	 */
	protected transient Pattern m_pattern;

	/**
	 * The last fwd pattern read
	 * @label fwdPattern
	 */
	protected transient Pattern b_pattern;
	protected transient int count = 0;
	protected transient boolean notFirstTime;
	protected transient boolean notFirstTimeB;
	protected transient Learner myLearner = null;

	// Objects used for synchronization
	protected transient volatile Object fwdLock = null;
	protected transient volatile Object revLock = null;

	/**
	 * Contains true if for the current Synapse must be used a Learner instead
	 * of a built-in learning algorithm. Set it in the constructor of any
	 * inherited class. Used by the getLearner method.
	 * 
	 * @see #getLearner
	 */
	protected boolean learnable = false;

	public Synapse() {
		// log.info ("Synapse instanciated");
	}

	/**
	 * Adds a noise to the weights of the synapse
	 * 
	 * @param amplitude Amplitude of the noise: the value is centered around the
	 *            zero. e.g.: an amplitude = 0.2 means a noise range from -0.2
	 *            to 0.2
	 */
	public void addNoise(double amplitude) {
		if (array != null)
			array.addNoise(amplitude);
	}

	/**
	 * Initializes all the weigths of the synapses with random values
	 * 
	 * @param amplitude Amplitude of the random values: the value is centered
	 *            around the zero. e.g.: an amplitude = 0.2 means a values'
	 *            range from -0.2 to 0.2
	 */
	public void randomize(double amplitude) {
		if (array != null)
			// array.randomize(-1.0 * amplitude, amplitude);
			array.initialize();
	}

	/**
	 * Funzione di TRAIN dell'elemento.
	 * 
	 * @param pattern double[] - pattern di input sul quale applicare la
	 *            funzione di trasferimento
	 */
	protected abstract void backward(double[] pattern);

	/**
	 * Returns TRUE if the synapse calls the method nextStep() on the Monitor
	 * object when the fwdGet() method is called
	 * 
	 * @return boolean
	 */
	public boolean canCountSteps() {
		return false;
	}

	/**
	 * Recall function
	 * 
	 * @param pattern double[] - input pattern
	 */
	protected abstract void forward(double[] pattern);

	public Pattern fwdGet() {
		if (!isEnabled())
			return null;
		synchronized (getFwdLock()) {
			if ((notFirstTime) || (!loopBack)) {
				while (items == 0) {
					try {
						fwdLock.wait();
					} catch (InterruptedException e) {
						// log.warn ( "wait () was interrupted");
						// e.printStackTrace();
						reset();
						fwdLock.notify();
						return null;
					}
				}
				--items;
				m_pattern.setArray(outs);
				if (isLoopBack())
					// To avoid sinc problems
					m_pattern.setCount(0);
				fwdLock.notify();
				return m_pattern;
			} else {
				items = bitems = count = 0;
				notFirstTime = true;
				fwdLock.notify();
				return null;
			}
		}
	}

	public void fwdPut(Pattern pattern) {
		if (isEnabled()) {
			synchronized (getFwdLock()) {
				while (items > 0) {
					try {
						fwdLock.wait();
					} catch (InterruptedException e) {
						reset();
						fwdLock.notify();
						return;
					} // End of catch
				}
				m_pattern = pattern;
				count = m_pattern.getCount();
				inps = (double[]) pattern.getArray();
				forward(inps);
				++items;
				fwdLock.notify();
			}
		}
	}

	/**
	 * Resets the internal state to be ready for the next run
	 */
	public void reset() {
		items = bitems = 0;
		notFirstTime = false;
		notFirstTimeB = false;
	}

	/**
	 * Returns the number of the ignored cycles at beginning of each epoch.
	 * During these cycles the synapse returns null on the call to the xxxGet
	 * methods
	 *
	 * @return int
	 * @see Synapse#setIgnoreBefore
	 */
	public int getIgnoreBefore() {
		return ignoreBefore;
	}

	/**
	 * Returns the input dimension of the synapse.
	 * 
	 * @return int
	 */
	public int getInputDimension() {
		return inputDimension;
	}

	/**
	 * Returns the value of the learning rate
	 * 
	 * @return double
	 */
	public double getLearningRate() {
		if (monitor != null)
			return monitor.getLearningRate();
		else
			return 0.0;
	}

	/**
	 * Returns the value of the momentum
	 * 
	 * @return double
	 */
	public double getMomentum() {
		if (monitor != null)
			return monitor.getMomentum();
		else
			return 0.0;
	}

	/**
	 * Returns the Monitor object attached to the synapse
	 * 
	 * @return neural.engine.Monitor
	 */
	public Monitor getMonitor() {
		return monitor;
	}

	/**
	 * Returns the name of the synapse
	 * 
	 * @return String
	 * @see #setName
	 */
	public String getName() {
		return fieldName;
	}

	/**
	 * Returns the output dimension of the synapse.
	 * 
	 * @return int
	 */
	public int getOutputDimension() {
		return outputDimension;
	}

	protected Object readResolve() {
		setArrays(getInputDimension(), getOutputDimension());
		return this;
	}

	public Pattern revGet() {
		if (!isEnabled())
			return null;
		synchronized (getRevLock()) {
			if ((notFirstTimeB) || (!loopBack)) {
				while (bitems == 0) {
					try {
						revLock.wait();
					} catch (InterruptedException e) {
						// log.warn ( "wait () was interrupted");
						// e.printStackTrace();
						reset();
						revLock.notify();
						return null;
					}
				}
				--bitems;
				b_pattern.setArray(bouts);
				revLock.notify();
				return b_pattern;
			} else {
				// bitems = 0;
				revLock.notify();
				return null;
			}
		}
	}

	public void revPut(Pattern pattern) {
		if (isEnabled()) {
			synchronized (getRevLock()) {
				while (bitems > 0) {
					try {
						revLock.wait();
					} catch (InterruptedException e) {
						reset();
						revLock.notify();
						return;
					}
				}
				b_pattern = pattern;
				count = b_pattern.getCount();
				backward(pattern.getArray());
				++bitems;
				notFirstTimeB = true;
				revLock.notify();
			}
		}
	}

	/**
	 * Insert the method's description here. Creation date: (23/09/2000
	 * 12.52.58)
	 */
	protected abstract void setArrays(int rows, int cols);

	/**
	 * Dimensiona l'elemento
	 * 
	 * @param  rows - righe
	 * @param  cols - colonne
	 */
	protected abstract void setDimensions(int rows, int cols);

	/**
	 * Sets the number of the ignored cycles at beginning of each epoch. During
	 * these cycles the synapse is disabled. Useful when the synapse is attached
	 * as the Input2 of a SwitchSynapse
	 *
	 * @param newIgnoreBefore int
	 * @see InputSwitchSynapse
	 */
	public void setIgnoreBefore(int newIgnoreBefore) {
		ignoreBefore = newIgnoreBefore;
	}

	/**
	 * Sets the input dimension of the synapse
	 * 
	 * @param newInputDimension int
	 */
	public void setInputDimension(int newInputDimension) {
		if (inputDimension != newInputDimension) {
			inputDimension = newInputDimension;
			setDimensions(newInputDimension, -1);
		}
	}

	/**
	 * Sets the value of the learning rate
	 * 
	 * @param newLearningRate double
	 */
	public void setLearningRate(double newLearningRate) {
		learningRate = newLearningRate;
	}

	/**
	 * Sets the value of the momentum rate
	 * 
	 * @param newMomentum double
	 */
	public void setMomentum(double newMomentum) {
		momentum = newMomentum;
	}

	/**
	 * Sets the Monitor object of the synapse
	 * 
	 * @param newMonitor neural.engine.Monitor
	 */
	public void setMonitor(Monitor newMonitor) {
		monitor = newMonitor;
		if (monitor != null) {
			setLearningRate(monitor.getLearningRate());
			setMomentum(monitor.getMomentum());
		}
	}

	/**
	 * Sets the name of the synapse
	 * 
	 * @param name The name of the component.
	 * @see #getName
	 */
	public void setName(java.lang.String name) {
		fieldName = name;
	}

	/**
	 * Sets the output dimension of the synapse
	 * 
	 * @param newOutputDimension int
	 */
	public void setOutputDimension(int newOutputDimension) {
		if (outputDimension != newOutputDimension) {
			outputDimension = newOutputDimension;
			setDimensions(-1, newOutputDimension);
		}
	}

	/**
	 * Getter for property enabled.
	 * 
	 * @return Value of property enabled.
	 */
	public boolean isEnabled() {
		return enabled;
	}

	/**
	 * Setter for property enabled.
	 * 
	 * @param enabled New value of property enabled.
	 */
	public void setEnabled(boolean enabled) {
		this.enabled = enabled;
	}

	/**
	 * Getter for property loopBack.
	 * 
	 * @return Value of property loopBack.
	 *
	 */
	public boolean isLoopBack() {
		return loopBack;
	}

	/**
	 * Setter for property loopBack.
	 * 
	 * @param loopBack New value of property loopBack.
	 *
	 */
	public void setLoopBack(boolean loopBack) {
		this.loopBack = loopBack;
	}

	/**
	 * Base for check messages. Subclasses should call this method from thier
	 * own check method.
	 *
	 * @see InputPatternListener
	 * @see OutputPatternListener
	 * @return validation errors.
	 */
	public TreeSet check() {

		// Prepare an empty set for check messages;
		TreeSet checks = new TreeSet();

		// Return check messages
		return checks;
	}

	public Collection Inspections() {
		Collection col = new ArrayList();
		col.add(new WeightsInspection(array));
		return col;
	}

	public String InspectableTitle() {
		return this.getName();
	}

	/**
	 * Getter for property inputFull.
	 * 
	 * @return Value of property inputFull.
	 *
	 */
	public boolean isInputFull() {
		return inputFull;
	}

	/**
	 * Setter for property inputFull.
	 * 
	 * @param inputFull New value of property inputFull.
	 *
	 */
	public void setInputFull(boolean inputFull) {
		this.inputFull = inputFull;
	}

	/**
	 * Getter for property outputFull.
	 * 
	 * @return Value of property outputFull.
	 *
	 */
	public boolean isOutputFull() {
		return outputFull;
	}

	/**
	 * Setter for property outputFull.
	 * 
	 * @param outputFull New value of property outputFull.
	 *
	 */
	public void setOutputFull(boolean outputFull) {
		this.outputFull = outputFull;
	}

	/**
	 * Getter for the internal matrix of weights
	 *
	 * @return the Matrix containing the 2D array of weights
	 */
	public Matrix getWeights() {
		return array;
	}

	/**
	 * Setter for the internal matrix of weights
	 *
	 * @param newWeights Matrix containing the 2D array of weights
	 */
	public void setWeights(Matrix newWeights) {
		array = newWeights;
	}

	/**
	 * Returns the appropriate Learner object for this class depending on the
	 * Monitor.learningMode property value
	 * 
	 * @return the Learner object if applicable, otherwise null
	 * @see org.joone.engine.Learnable#getLearner()
	 */
	public Learner getLearner() {
		if (!learnable) {
			return null;
		}
		return getMonitor().getLearner();
	}

	/**
	 * Initialize the Learner object
	 * 
	 * @see org.joone.engine.Learnable#initLearner()
	 */
	public void initLearner() {
		myLearner = getLearner();
		if (myLearner != null) {
			myLearner.registerLearnable(this);
		}
	}

	/**
	 * Getter for property fwdLock.
	 * 
	 * @return Value of property fwdLock.
	 */
	protected Object getFwdLock() {
		if (fwdLock == null)
			fwdLock = new Object();
		return fwdLock;
	}

	/**
	 * Getter for property revLock.
	 * 
	 * @return Value of property revLock.
	 */
	protected Object getRevLock() {
		if (revLock == null)
			revLock = new Object();
		return revLock;
	}

	/**
	 * Synapse's initialization. It needs to be invoked at the starting of the
	 * neural network It's called within the Layer.init() method
	 */
	public void init() {
		this.initLearner();
		this.getFwdLock();
		this.getRevLock();
	}

}